/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.cf.direct;

import com.android.dx.cf.attrib.AttSourceFile;
import com.android.dx.cf.cst.ConstantPoolParser;
import com.android.dx.cf.iface.Attribute;
import com.android.dx.cf.iface.AttributeList;
import com.android.dx.cf.iface.ClassFile;
import com.android.dx.cf.iface.FieldList;
import com.android.dx.cf.iface.MethodList;
import com.android.dx.cf.iface.ParseException;
import com.android.dx.cf.iface.ParseObserver;
import com.android.dx.cf.iface.StdAttributeList;
import com.android.dx.rop.code.AccessFlags;
import com.android.dx.rop.cst.ConstantPool;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.cst.StdConstantPool;
import com.android.dx.rop.type.StdTypeList;
import com.android.dx.rop.type.Type;
import com.android.dx.rop.type.TypeList;
import com.android.dx.util.ByteArray;
import com.android.dx.util.Hex;

/**
 * Class file with info taken from a {@code byte[]} or slice thereof.
 */
public class DirectClassFile implements ClassFile {

	/** the expected value of the ClassFile.magic field */
	private static final int CLASS_FILE_MAGIC = 0xcafebabe;

	/**
	 * minimum {@code .class} file major version See
	 * http://en.wikipedia.org/wiki/Java_class_file for an up-to-date list of
	 * version numbers. Currently known (taken from that table) are: J2SE 6.0 =
	 * 50 (0x32 hex), J2SE 5.0 = 49 (0x31 hex), JDK 1.4 = 48 (0x30 hex), JDK 1.3
	 * = 47 (0x2F hex), JDK 1.2 = 46 (0x2E hex), JDK 1.1 = 45 (0x2D hex). Valid
	 * ranges are typically of the form "A.0 through B.C inclusive" where A <= B
	 * and C >= 0, which is why we don't have a CLASS_FILE_MIN_MINOR_VERSION.
	 */
	private static final int CLASS_FILE_MIN_MAJOR_VERSION = 45;

	/**
	 * maximum {@code .class} file major version Note: if you change this,
	 * please change "java.class.version" in System.java.
	 */
	private static final int CLASS_FILE_MAX_MAJOR_VERSION = 50;

	/** maximum {@code .class} file minor version */
	private static final int CLASS_FILE_MAX_MINOR_VERSION = 0;

	/**
	 * {@code non-null;} the file path for the class, excluding any base
	 * directory specification
	 */
	private final String filePath;

	/** {@code non-null;} the bytes of the file */
	private final ByteArray bytes;

	/**
	 * whether to be strict about parsing; if {@code false}, this avoids doing
	 * checks that only exist for purposes of verification (such as magic number
	 * matching and path-package consistency checking)
	 */
	private final boolean strictParse;

	/**
	 * {@code null-ok;} the constant pool; only ever {@code null} before the
	 * constant pool is successfully parsed
	 */
	private StdConstantPool pool;

	/**
	 * the class file field {@code access_flags}; will be {@code -1} before the
	 * file is successfully parsed
	 */
	private int accessFlags;

	/**
	 * {@code null-ok;} the class file field {@code this_class}, interpreted as
	 * a type constant; only ever {@code null} before the file is successfully
	 * parsed
	 */
	private CstType thisClass;

	/**
	 * {@code null-ok;} the class file field {@code super_class}, interpreted as
	 * a type constant if non-zero
	 */
	private CstType superClass;

	/**
	 * {@code null-ok;} the class file field {@code interfaces}; only ever
	 * {@code null} before the file is successfully parsed
	 */
	private TypeList interfaces;

	/**
	 * {@code null-ok;} the class file field {@code fields}; only ever
	 * {@code null} before the file is successfully parsed
	 */
	private FieldList fields;

	/**
	 * {@code null-ok;} the class file field {@code methods}; only ever
	 * {@code null} before the file is successfully parsed
	 */
	private MethodList methods;

	/**
	 * {@code null-ok;} the class file field {@code attributes}; only ever
	 * {@code null} before the file is successfully parsed
	 */
	private StdAttributeList attributes;

	/** {@code null-ok;} attribute factory, if any */
	private AttributeFactory attributeFactory;

	/** {@code null-ok;} parse observer, if any */
	private ParseObserver observer;

	/**
	 * Returns the string form of an object or {@code "(none)"} (rather than
	 * {@code "null"}) for {@code null}.
	 * 
	 * @param obj
	 *            {@code null-ok;} the object to stringify
	 * @return {@code non-null;} the appropriate string form
	 */
	public static String stringOrNone(Object obj) {
		if (obj == null) {
			return "(none)";
		}

		return obj.toString();
	}

	/**
	 * Constructs an instance.
	 * 
	 * @param bytes
	 *            {@code non-null;} the bytes of the file
	 * @param filePath
	 *            {@code non-null;} the file path for the class, excluding any
	 *            base directory specification
	 * @param strictParse
	 *            whether to be strict about parsing; if {@code false}, this
	 *            avoids doing checks that only exist for purposes of
	 *            verification (such as magic number matching and path-package
	 *            consistency checking)
	 */
	public DirectClassFile(ByteArray bytes, String filePath, boolean strictParse) {
		if (bytes == null) {
			throw new NullPointerException("bytes == null");
		}

		if (filePath == null) {
			throw new NullPointerException("filePath == null");
		}

		this.filePath = filePath;
		this.bytes = bytes;
		this.strictParse = strictParse;
		this.accessFlags = -1;
	}

	/**
	 * Constructs an instance.
	 * 
	 * @param bytes
	 *            {@code non-null;} the bytes of the file
	 * @param filePath
	 *            {@code non-null;} the file path for the class, excluding any
	 *            base directory specification
	 * @param strictParse
	 *            whether to be strict about parsing; if {@code false}, this
	 *            avoids doing checks that only exist for purposes of
	 *            verification (such as magic number matching and path-package
	 *            consistency checking)
	 */
	public DirectClassFile(byte[] bytes, String filePath, boolean strictParse) {
		this(new ByteArray(bytes), filePath, strictParse);
	}

	/**
	 * Sets the parse observer for this instance.
	 * 
	 * @param observer
	 *            {@code null-ok;} the observer
	 */
	public void setObserver(ParseObserver observer) {
		this.observer = observer;
	}

	/**
	 * Sets the attribute factory to use.
	 * 
	 * @param attributeFactory
	 *            {@code non-null;} the attribute factory
	 */
	public void setAttributeFactory(AttributeFactory attributeFactory) {
		if (attributeFactory == null) {
			throw new NullPointerException("attributeFactory == null");
		}

		this.attributeFactory = attributeFactory;
	}

	/**
	 * Gets the {@link ByteArray} that this instance's data comes from.
	 * 
	 * @return {@code non-null;} the bytes
	 */
	public ByteArray getBytes() {
		return bytes;
	}

	/** {@inheritDoc} */
	public int getMagic() {
		parseToInterfacesIfNecessary();
		return getMagic0();
	}

	/** {@inheritDoc} */
	public int getMinorVersion() {
		parseToInterfacesIfNecessary();
		return getMinorVersion0();
	}

	/** {@inheritDoc} */
	public int getMajorVersion() {
		parseToInterfacesIfNecessary();
		return getMajorVersion0();
	}

	/** {@inheritDoc} */
	public int getAccessFlags() {
		parseToInterfacesIfNecessary();
		return accessFlags;
	}

	/** {@inheritDoc} */
	public CstType getThisClass() {
		parseToInterfacesIfNecessary();
		return thisClass;
	}

	/** {@inheritDoc} */
	public CstType getSuperclass() {
		parseToInterfacesIfNecessary();
		return superClass;
	}

	/** {@inheritDoc} */
	public ConstantPool getConstantPool() {
		parseToInterfacesIfNecessary();
		return pool;
	}

	/** {@inheritDoc} */
	public TypeList getInterfaces() {
		parseToInterfacesIfNecessary();
		return interfaces;
	}

	/** {@inheritDoc} */
	public FieldList getFields() {
		parseToEndIfNecessary();
		return fields;
	}

	/** {@inheritDoc} */
	public MethodList getMethods() {
		parseToEndIfNecessary();
		return methods;
	}

	/** {@inheritDoc} */
	public AttributeList getAttributes() {
		parseToEndIfNecessary();
		return attributes;
	}

	/** {@inheritDoc} */
	public CstString getSourceFile() {
		AttributeList attribs = getAttributes();
		Attribute attSf = attribs.findFirst(AttSourceFile.ATTRIBUTE_NAME);

		if (attSf instanceof AttSourceFile) {
			return ((AttSourceFile) attSf).getSourceFile();
		}

		return null;
	}

	/**
	 * Constructs and returns an instance of {@link TypeList} whose data comes
	 * from the bytes of this instance, interpreted as a list of constant pool
	 * indices for classes, which are in turn translated to type constants.
	 * Instance construction will fail if any of the (alleged) indices turn out
	 * not to refer to constant pool entries of type {@code Class}.
	 * 
	 * @param offset
	 *            offset into {@link #bytes} for the start of the data
	 * @param size
	 *            number of elements in the list (not number of bytes)
	 * @return {@code non-null;} an appropriately-constructed class list
	 */
	public TypeList makeTypeList(int offset, int size) {
		if (size == 0) {
			return StdTypeList.EMPTY;
		}

		if (pool == null) {
			throw new IllegalStateException("pool not yet initialized");
		}

		return new DcfTypeList(bytes, offset, size, pool, observer);
	}

	/**
	 * Gets the class file field {@code magic}, but without doing any checks or
	 * parsing first.
	 * 
	 * @return the magic value
	 */
	public int getMagic0() {
		return bytes.getInt(0);
	}

	/**
	 * Gets the class file field {@code minor_version}, but without doing any
	 * checks or parsing first.
	 * 
	 * @return the minor version
	 */
	public int getMinorVersion0() {
		return bytes.getUnsignedShort(4);
	}

	/**
	 * Gets the class file field {@code major_version}, but without doing any
	 * checks or parsing first.
	 * 
	 * @return the major version
	 */
	public int getMajorVersion0() {
		return bytes.getUnsignedShort(6);
	}

	/**
	 * Runs {@link #parse} if it has not yet been run to cover up to the
	 * interfaces list.
	 */
	private void parseToInterfacesIfNecessary() {
		if (accessFlags == -1) {
			parse();
		}
	}

	/**
	 * Runs {@link #parse} if it has not yet been run successfully.
	 */
	private void parseToEndIfNecessary() {
		if (attributes == null) {
			parse();
		}
	}

	/**
	 * Does the parsing, handing exceptions.
	 */
	private void parse() {
		try {
			parse0();
		} catch (ParseException ex) {
			ex.addContext("...while parsing " + filePath);
			throw ex;
		} catch (RuntimeException ex) {
			ParseException pe = new ParseException(ex);
			pe.addContext("...while parsing " + filePath);
			throw pe;
		}
	}

	/**
	 * Sees if the .class file header magic/version are within range.
	 * 
	 * @param magic
	 *            the value of a classfile "magic" field
	 * @param minorVersion
	 *            the value of a classfile "minor_version" field
	 * @param majorVersion
	 *            the value of a classfile "major_version" field
	 * @return true iff the parameters are valid and within range
	 */
	private boolean isGoodVersion(int magic, int minorVersion, int majorVersion) {
		/*
		 * Valid version ranges are typically of the form
		 * "A.0 through B.C inclusive" where A <= B and C >= 0, which is why we
		 * don't have a CLASS_FILE_MIN_MINOR_VERSION.
		 */
		if (magic == CLASS_FILE_MAGIC && minorVersion >= 0) {
			/*
			 * Check against max first to handle the case where MIN_MAJOR ==
			 * MAX_MAJOR.
			 */
			if (majorVersion == CLASS_FILE_MAX_MAJOR_VERSION) {
				if (minorVersion <= CLASS_FILE_MAX_MINOR_VERSION) {
					return true;
				}
			} else if (majorVersion < CLASS_FILE_MAX_MAJOR_VERSION
					&& majorVersion >= CLASS_FILE_MIN_MAJOR_VERSION) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Does the actual parsing.
	 */
	private void parse0() {
		if (bytes.size() < 10) {
			throw new ParseException("severely truncated class file");
		}

		if (observer != null) {
			observer.parsed(bytes, 0, 0, "begin classfile");
			observer.parsed(bytes, 0, 4, "magic: " + Hex.u4(getMagic0()));
			observer.parsed(bytes, 4, 2,
					"minor_version: " + Hex.u2(getMinorVersion0()));
			observer.parsed(bytes, 6, 2,
					"major_version: " + Hex.u2(getMajorVersion0()));
		}

		if (strictParse) {
			/*
			 * Make sure that this looks like a valid class file with a version
			 * that we can handle.
			 */
			if (!isGoodVersion(getMagic0(), getMinorVersion0(),
					getMajorVersion0())) {
				throw new ParseException("bad class file magic ("
						+ Hex.u4(getMagic0()) + ") or version ("
						+ Hex.u2(getMajorVersion0()) + "."
						+ Hex.u2(getMinorVersion0()) + ")");
			}
		}

		ConstantPoolParser cpParser = new ConstantPoolParser(bytes);
		cpParser.setObserver(observer);
		pool = cpParser.getPool();
		pool.setImmutable();

		int at = cpParser.getEndOffset();
		int accessFlags = bytes.getUnsignedShort(at); // u2 access_flags;
		int cpi = bytes.getUnsignedShort(at + 2); // u2 this_class;
		thisClass = (CstType) pool.get(cpi);
		cpi = bytes.getUnsignedShort(at + 4); // u2 super_class;
		superClass = (CstType) pool.get0Ok(cpi);
		int count = bytes.getUnsignedShort(at + 6); // u2 interfaces_count

		if (observer != null) {
			observer.parsed(bytes, at, 2,
					"access_flags: " + AccessFlags.classString(accessFlags));
			observer.parsed(bytes, at + 2, 2, "this_class: " + thisClass);
			observer.parsed(bytes, at + 4, 2, "super_class: "
					+ stringOrNone(superClass));
			observer.parsed(bytes, at + 6, 2,
					"interfaces_count: " + Hex.u2(count));
			if (count != 0) {
				observer.parsed(bytes, at + 8, 0, "interfaces:");
			}
		}

		at += 8;
		interfaces = makeTypeList(at, count);
		at += count * 2;

		if (strictParse) {
			/*
			 * Make sure that the file/jar path matches the declared
			 * package/class name.
			 */
			String thisClassName = thisClass.getClassType().getClassName();
			if (!(filePath.endsWith(".class")
					&& filePath.startsWith(thisClassName) && (filePath.length() == (thisClassName
					.length() + 6)))) {
				throw new ParseException("class name (" + thisClassName
						+ ") does not match path (" + filePath + ")");
			}
		}

		/*
		 * Only set the instance variable accessFlags here, since that's what
		 * signals a successful parse of the first part of the file (through the
		 * interfaces list).
		 */
		this.accessFlags = accessFlags;

		FieldListParser flParser = new FieldListParser(this, thisClass, at,
				attributeFactory);
		flParser.setObserver(observer);
		fields = flParser.getList();
		at = flParser.getEndOffset();

		MethodListParser mlParser = new MethodListParser(this, thisClass, at,
				attributeFactory);
		mlParser.setObserver(observer);
		methods = mlParser.getList();
		at = mlParser.getEndOffset();

		AttributeListParser alParser = new AttributeListParser(this,
				AttributeFactory.CTX_CLASS, at, attributeFactory);
		alParser.setObserver(observer);
		attributes = alParser.getList();
		attributes.setImmutable();
		at = alParser.getEndOffset();

		if (at != bytes.size()) {
			throw new ParseException("extra bytes at end of class file, "
					+ "at offset " + Hex.u4(at));
		}

		if (observer != null) {
			observer.parsed(bytes, at, 0, "end classfile");
		}
	}

	/**
	 * Implementation of {@link TypeList} whose data comes directly from the
	 * bytes of an instance of this (outer) class, interpreted as a list of
	 * constant pool indices for classes which are in turn returned as type
	 * constants. Instance construction will fail if any of the (alleged)
	 * indices turn out not to refer to constant pool entries of type
	 * {@code Class}.
	 */
	private static class DcfTypeList implements TypeList {

		/** {@code non-null;} array containing the data */
		private final ByteArray bytes;

		/** number of elements in the list (not number of bytes) */
		private final int size;

		/** {@code non-null;} the constant pool */
		private final StdConstantPool pool;

		/**
		 * Constructs an instance.
		 * 
		 * @param bytes
		 *            {@code non-null;} original classfile's bytes
		 * @param offset
		 *            offset into {@link #bytes} for the start of the data
		 * @param size
		 *            number of elements in the list (not number of bytes)
		 * @param pool
		 *            {@code non-null;} the constant pool to use
		 * @param observer
		 *            {@code null-ok;} parse observer to use, if any
		 */
		public DcfTypeList(ByteArray bytes, int offset, int size,
				StdConstantPool pool, ParseObserver observer) {
			if (size < 0) {
				throw new IllegalArgumentException("size < 0");
			}

			bytes = bytes.slice(offset, offset + size * 2);
			this.bytes = bytes;
			this.size = size;
			this.pool = pool;

			for (int i = 0; i < size; i++) {
				offset = i * 2;
				int idx = bytes.getUnsignedShort(offset);
				CstType type;
				try {
					type = (CstType) pool.get(idx);
				} catch (ClassCastException ex) {
					// Translate the exception.
					throw new RuntimeException("bogus class cpi", ex);
				}
				if (observer != null) {
					observer.parsed(bytes, offset, 2, "  " + type);
				}
			}
		}

		/** {@inheritDoc} */
		public boolean isMutable() {
			return false;
		}

		/** {@inheritDoc} */
		public int size() {
			return size;
		}

		/** {@inheritDoc} */
		public int getWordCount() {
			// It is the same as size because all elements are classes.
			return size;
		}

		/** {@inheritDoc} */
		public Type getType(int n) {
			int idx = bytes.getUnsignedShort(n * 2);
			return ((CstType) pool.get(idx)).getClassType();
		}

		/** {@inheritDoc} */
		public TypeList withAddedType(Type type) {
			throw new UnsupportedOperationException("unsupported");
		}
	}
}
